/*
 * OpenParts
 * A dynamic-loading components framework for GWT
 * Copyright (C) 2011 Christophe Bouthier  [chris{AT}binary-gastronome{DOT}fr]
 *
 * This work is partially based on work I have done at INRIA (http://www.inria.fr) 
 * in the context of the Qualipso European Project (http://qualipso.org/),
 * The work done at INRIA is Copyright (C) 2006-2011 INRIA
 *
 * This work is distributed under the LGPL version 3
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation. See the GNU
 * Lesser General Public License in LGPL.txt for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package fr.openparts.OpenParts.client;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Vector;

import com.google.gwt.core.client.Duration;
import com.google.gwt.user.client.Timer;

/**
 * Scheduler module, in charge of the command queue. Execute commands synchronously, even for asynchronous command like loading.
 * 
 * @author Christophe Bouthier [chris{AT}binary-gastronome{DOT}fr]
 * @creation.date 09 November 2010
 */
public class OPScheduler {
    
    /**
     * Maximum amount of time spend executing commands before yielding to the system
     */
    protected final static int TIME_SLICE = 100;

    /**
     * Delay between checking if commands execution loop was stopped because of SSW
     */
    protected final static int TIME_RESCUE = 50;

    /**
     * Flag indicating if an asynchronous command is in progress.
     */
    protected boolean asynchronousInProgress = false;

    /**
     * Flag indicating if the loop execution was postponed because of a time slice exceed.
     */
    protected boolean executionPostponed = false;

    /**
     * Flag indicating that a command is executing. Used to check for SSW interruption.
     */
    protected boolean commandIsExecuting = false;

    /**
     * Queue of command to execute.
     */
    protected final Queue<OPCommand> commandQueue = new LinkedList<OPCommand>();

    /**
     * Timer to wake up the commands execution loop after yield to the system, due to time slice exceeded.
     */
    protected Timer wakeUpTimer = null;

    /**
     * Timer to rescue the commands execution loop after a SSW stop.
     */
    protected Timer rescueTimer = null;

    /**
     * Create and bind the scheduler module.
     */
    public final static void bind() {
        final OPScheduler scheduler = new OPScheduler();
        final OPSchedulerBinding schedulerBinding = OPSchedulerBinding.schedulerBindingFactory();

        scheduler.registerMeAsBinding(schedulerBinding);
        OPShell.opBindingSharedInstance().bind(schedulerBinding);
    }

    /**
     * Register this scheduler as the scheduler of the given scheduler binding.
     * 
     * @param schedulerBinding
     *            the scheduler binding to register into. Shouldn't be null.
     */
    private void registerMeAsBinding(final OPSchedulerBinding schedulerBinding) {
        registerMeAsCommandSchedulingHandler(this, schedulerBinding);
    }

    /**
     * Protected constructor, should use the bind static method.
     */
    protected OPScheduler() {}

    /**
     * Register the given scheduler as the command scheduling handler for the given scheduler binding.
     * 
     * @param scheduler
     *            the given scheduler to register as command scheduling handler
     * @param schedulerBinding
     *            the given scheduling binding on which to register the scheduling
     */
    protected final native void registerMeAsCommandSchedulingHandler(final OPScheduler scheduler, final OPSchedulerBinding schedulerBinding) /*-{
        schedulerBinding.scheduleCommand = function(command) {
        return $entry(scheduler.@fr.openparts.OpenParts.client.OPScheduler::scheduleCommand(Lfr/openparts/OpenParts/client/OPCommand;)(command));
        };
    }-*/;

    /**
     * Command scheduling handler, called when a new command is sent. This method should not be called directly, it is called by {@link OPShell}.
     * 
     * @param command
     *            the command to schedule. Shouldn't be null.
     */
    public void scheduleCommand(final OPCommand command) {
        assert command != null : "parameter command shouldn't be null";
        
        // check if there is a need for a pre command
        final OPCommand preCommand = OPCommandFactory.getPreCommand(command);
        if (preCommand != null) {
            // yes, there is a pre command, put it first in the queue
            commandQueue.add(preCommand);
        }

        // add the command to the queue
        commandQueue.add(command);

        // finally, try to launch next commands in the queue if there is no command executing yet
        maybeLaunchCommands();
    }

    /**
     * Try to launch next commands in the queue, if there is no command executing yet.
     */
    protected void maybeLaunchCommands() {
        // check if there is an asynchronous command already in progress, or if the loop execution was postponed (because of time slice exceeded), or if a command is already executing
        // if yes, then return directly
        if (asynchronousInProgress || executionPostponed || commandIsExecuting) {
            return;
        }

        // there is nothing in progress right now, so we can launch next commands in the queue
        launchCommands();
    }

    /**
     * Launch the next commands in the queue.
     */
    protected void launchCommands() {
        // at the beginning, nothing is in progress
        asynchronousInProgress = false;
        executionPostponed = false;
        commandIsExecuting = false;

        // save the starting time to check that we don't exceed the slice
        final double startTime = Duration.currentTimeMillis();

        // launch the rescue timer, to check for SSW interruption
        launchRescueTimer();

        while (!commandQueue.isEmpty()) {
            // get the next command in the queue
            final OPCommand currentCommand = commandQueue.poll();
            assert currentCommand != null : "command shouldn't be null";

            // execute it
            commandIsExecuting = true;
            boolean commandExecutionFinised = OPCommandFactory.execute(currentCommand, this);
            commandIsExecuting = false;
            
            // check if the execution of the command finished, so that we can execute the next command
            if (!commandExecutionFinised) {
                // no, the command didn't finished executing (it is asynchronous).
                // set the flag
                asynchronousInProgress = true;

                // stop the loop, the command will call us back when it finished.
                break;
            }

            // check if we didn't exceed the time slice
            if (Duration.currentTimeMillis() - startTime >= TIME_SLICE) {
                // yes we did. We need to yield to the system.
                // put a timer to wake up back after that
                launchWakeUpThread();

                // set the flag
                executionPostponed = true;

                // yield to the system
                break;
            }

            // here, we executed a command that is not asynchronous, and we didn't exceed the time slice, so we can loop back and execute the next
            // command in the queue.
        }
        
        // here, the execution loop stopped, so stop the rescue timer
        rescueTimer.cancel();

    }

    /**
     * Launch a rescue timer, that will check periodically that the commands execution loop wasn't stopped by a SSW.
     */
    protected void launchRescueTimer() {
        rescueTimer = new Timer() {
            @Override
            public void run() {
                checkLoop();
            }
        };
        rescueTimer.scheduleRepeating(TIME_RESCUE);
    }

    /**
     * Called by the rescue timer. Check if the loop has been stopped by a SSW, and relaunch it if it is the case.
     */
    protected void checkLoop() {
        if (commandIsExecuting) {
            // as javascript is single thread, if we are here, it means that the loop has been stopped by a SSW while running a command.
            // if it is the case, then relaunch the commands execution loop.
            // WARNING: it seems that on some browsers, if a command display an alert, then the rescueTimer can trigger even if the command is not finished.
            // if it is the case, then it will mistake this for a SSW
            launchCommands();
        }
    }

    /**
     * Launch a wake up timer, that will wake up the commands execution loop once we yield to the system due to time slice exceeded.
     */
    protected void launchWakeUpThread() {
        wakeUpTimer = new Timer() {
            @Override
            public void run() {
                executionPostponed = false;
                maybeLaunchCommands();
            }
        };
        // As we put a time of 1, it is guaranteed to be run at the end of the current javascript event loop.
        wakeUpTimer.schedule(1);
    }

    /**
     * Try to launch next commands in the queue, after an async command terminated.
     * Shouldn't be called directly, except by {@link OPCommandFactory}.
     */
    public void maybeLaunchCommandsAfterAsyncCommand() {
        asynchronousInProgress = false;
        maybeLaunchCommands();
    }
    
    /**
     * Remove and returns all commands depending on the given service.
     * 
     * @param service
     *            the service to check for. Shouldn't be null.
     * @return an array of all commands depending on the given services. Those commands have been removed from the execution queue. Can be empty, but cannot be
     *         null.
     */
    public OPCommand[] removeAllDependingCommands(final String service) {
        assert service != null : "parameter service shouldn't be null";

        final Vector<OPCommand> commands = new Vector<OPCommand>();

        // check all commands services
        for (OPCommand command : commandQueue) {
            String commandService = command.getService();
            if (service.equals(commandService)) {
                commands.add(command);
            }
        }

        // remove the commands
        // this is done after the first loop, to prevent any problem that can occurs when modifying a container on which we are iterating at the same time
        for (OPCommand command : commands) {
            commandQueue.remove(command);
        }

        return commands.toArray(new OPCommand[0]);
    }
}
